/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.handler.component;

import org.apache.lucene.search.FieldComparatorSource;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.PriorityQueue;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.search.MissingStringLastComparatorSource;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

public class ShardDoc extends FieldDoc {
	public String shard;
	public String shardAddress; // TODO

	int orderInShard;
	// the position of this doc within the shard... this can be used
	// to short-circuit comparisons if the shard is equal, and can
	// also be used to break ties within the same shard.

	public Object id;
	// this is currently the uniqueKeyField but
	// may be replaced with internal docid in a future release.

	Float score;

	NamedList sortFieldValues;
	// sort field values for *all* docs in a particular shard.
	// this doc's values are in position orderInShard

	// TODO: store the SolrDocument here?
	// Store the order in the merged list for lookup when getting stored fields?
	// (other components need this ordering to store data in order, like
	// highlighting)
	// but we shouldn't expose uniqueKey (have a map by it) until the
	// stored-field
	// retrieval stage.

	public int positionInResponse;

	// the ordinal position in the merged response arraylist

	public ShardDoc(float score, Object[] fields, Object uniqueId, String shard) {
		super(-1, score, fields);
		this.id = uniqueId;
		this.shard = shard;
	}

	public ShardDoc() {
		super(-1, Float.NaN);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;

		ShardDoc shardDoc = (ShardDoc) o;

		if (id != null ? !id.equals(shardDoc.id) : shardDoc.id != null)
			return false;

		return true;
	}

	@Override
	public int hashCode() {
		return id != null ? id.hashCode() : 0;
	}

	@Override
	public String toString() {
		return "id=" + id + " ,score=" + score + " ,shard=" + shard
				+ " ,orderInShard=" + orderInShard + " ,positionInResponse="
				+ positionInResponse + " ,sortFieldValues=" + sortFieldValues;
	}
}

// used by distributed search to merge results.
class ShardFieldSortedHitQueue extends PriorityQueue<ShardDoc> {

	/** Stores a comparator corresponding to each field being sorted by */
	protected Comparator[] comparators;

	/** Stores the sort criteria being used. */
	protected SortField[] fields;

	/**
	 * The order of these fieldNames should correspond to the order of sort
	 * field values retrieved from the shard
	 */
	protected List<String> fieldNames = new ArrayList<String>();

	public ShardFieldSortedHitQueue(SortField[] fields, int size) {
		final int n = fields.length;
		comparators = new Comparator[n];
		this.fields = new SortField[n];
		for (int i = 0; i < n; ++i) {

			// keep track of the named fields
			int type = fields[i].getType();
			if (type != SortField.SCORE && type != SortField.DOC) {
				fieldNames.add(fields[i].getField());
			}

			String fieldname = fields[i].getField();
			comparators[i] = getCachedComparator(fieldname,
					fields[i].getType(), fields[i].getLocale(),
					fields[i].getComparatorSource());

			if (fields[i].getType() == SortField.STRING) {
				this.fields[i] = new SortField(fieldname,
						fields[i].getLocale(), fields[i].getReverse());
			} else {
				this.fields[i] = new SortField(fieldname, fields[i].getType(),
						fields[i].getReverse());
			}

			// System.out.println("%%%%%%%%%%%%%%%%%% got "+fields[i].getType()
			// +"   for "+ fieldname
			// +"  fields[i].getReverse(): "+fields[i].getReverse());
		}

		initialize(size);
	}

	@Override
	protected boolean lessThan(ShardDoc docA, ShardDoc docB) {
		// If these docs are from the same shard, then the relative order
		// is how they appeared in the response from that shard.
		if (docA.shard == docB.shard) {
			// if docA has a smaller position, it should be "larger" so it
			// comes before docB.
			// This will handle sorting by docid within the same shard

			// comment this out to test comparators.
			return !(docA.orderInShard < docB.orderInShard);
		}

		// run comparators
		final int n = comparators.length;
		int c = 0;
		for (int i = 0; i < n && c == 0; i++) {
			c = (fields[i].getReverse()) ? comparators[i].compare(docB, docA)
					: comparators[i].compare(docA, docB);
		}

		// solve tiebreaks by comparing shards (similar to using docid)
		// smaller docid's beat larger ids, so reverse the natural ordering
		if (c == 0) {
			c = -docA.shard.compareTo(docB.shard);
		}

		return c < 0;
	}

	Comparator getCachedComparator(String fieldname, int type, Locale locale,
			FieldComparatorSource factory) {
		Comparator comparator = null;
		switch (type) {
		case SortField.SCORE:
			comparator = comparatorScore(fieldname);
			break;
		case SortField.STRING:
			if (locale != null)
				comparator = comparatorStringLocale(fieldname, locale);
			else
				comparator = comparatorNatural(fieldname);
			break;
		case SortField.CUSTOM:
			if (factory instanceof MissingStringLastComparatorSource) {
				comparator = comparatorMissingStringLast(fieldname);
			} else {
				// TODO: support other types such as random... is there a way to
				// support generically? Perhaps just comparing Object
				comparator = comparatorNatural(fieldname);
				// throw new
				// RuntimeException("Custom sort not supported factory is "+factory.getClass());
			}
			break;
		case SortField.DOC:
			// TODO: we can support this!
			throw new RuntimeException("Doc sort not supported");
		default:
			comparator = comparatorNatural(fieldname);
			break;
		}
		return comparator;
	}

	class ShardComparator implements Comparator {
		String fieldName;
		int fieldNum;

		public ShardComparator(String fieldName) {
			this.fieldName = fieldName;
			this.fieldNum = 0;
			for (int i = 0; i < fieldNames.size(); i++) {
				if (fieldNames.get(i).equals(fieldName)) {
					this.fieldNum = i;
					break;
				}
			}
		}

		Object sortVal(ShardDoc shardDoc) {
			assert (shardDoc.sortFieldValues.getName(fieldNum)
					.equals(fieldName));
			List lst = (List) shardDoc.sortFieldValues.getVal(fieldNum);
			return lst.get(shardDoc.orderInShard);
		}

		public int compare(Object o1, Object o2) {
			return 0;
		}
	}

	static Comparator comparatorScore(final String fieldName) {
		return new Comparator() {
			public final int compare(final Object o1, final Object o2) {
				ShardDoc e1 = (ShardDoc) o1;
				ShardDoc e2 = (ShardDoc) o2;

				final float f1 = e1.score;
				final float f2 = e2.score;
				if (f1 < f2)
					return -1;
				if (f1 > f2)
					return 1;
				return 0;
			}
		};
	}

	// The lucene natural sort ordering corresponds to numeric
	// and string natural sort orderings (ascending). Since
	// the PriorityQueue keeps the biggest elements by default,
	// we need to reverse the natural compare ordering so that the
	// smallest elements are kept instead of the largest... hence
	// the negative sign on the final compareTo().
	Comparator comparatorNatural(String fieldName) {
		return new ShardComparator(fieldName) {
			@Override
			public final int compare(final Object o1, final Object o2) {
				ShardDoc sd1 = (ShardDoc) o1;
				ShardDoc sd2 = (ShardDoc) o2;
				Comparable v1 = (Comparable) sortVal(sd1);
				Comparable v2 = (Comparable) sortVal(sd2);
				if (v1 == v2)
					return 0;
				if (v1 == null)
					return 1;
				if (v2 == null)
					return -1;
				return -v1.compareTo(v2);
			}
		};
	}

	Comparator comparatorStringLocale(final String fieldName, Locale locale) {
		final Collator collator = Collator.getInstance(locale);
		return new ShardComparator(fieldName) {
			@Override
			public final int compare(final Object o1, final Object o2) {
				ShardDoc sd1 = (ShardDoc) o1;
				ShardDoc sd2 = (ShardDoc) o2;
				Comparable v1 = (Comparable) sortVal(sd1);
				Comparable v2 = (Comparable) sortVal(sd2);
				if (v1 == v2)
					return 0;
				if (v1 == null)
					return 1;
				if (v2 == null)
					return -1;
				return -collator.compare(v1, v2);
			}
		};
	}

	Comparator comparatorMissingStringLast(final String fieldName) {
		return new ShardComparator(fieldName) {
			@Override
			public final int compare(final Object o1, final Object o2) {
				ShardDoc sd1 = (ShardDoc) o1;
				ShardDoc sd2 = (ShardDoc) o2;
				Comparable v1 = (Comparable) sortVal(sd1);
				Comparable v2 = (Comparable) sortVal(sd2);
				if (v1 == v2)
					return 0;
				if (v1 == null)
					return -1;
				if (v2 == null)
					return 1;
				return -v1.compareTo(v2);
			}
		};
	}

}
